# -*- mode: python; coding: utf-8; -*-
##--------------------------------------------------------------------------##
## NPSolitaire:
## Copyright (C) 2010-2013 Jeremy Austin-Bardo <tjaustinbardo@gmail.com>
##
## from PySolFC code:
## Copyright (C) 1998-2003 Markus Oberhumer <markus@oberhumer.com>
## Copyright (C) 2003 Mt. Hood Playing Card Co.
## Copyright (C) 2005-2010 Skomoroh <skomoroh@users.sourceforge.net>
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, version 3 of the License.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program.  If not, see <http://www.gnu.org/licenses/>.
##
##--------------------------------------------------------------------------##
"""
Card/tile pack mix-in support. Handles load, shuffle and placement of game pack.
"""
__docformat__ = 'restructuredtext en'
#_ = lambda n: n

import logging

LOGGER = logging.getLogger('NPS.GameLib.PackMixin')

from ..packlib.card import Card as CardItem
from ..packlib.data import CARD_DB
from ..packlib import info as CSI
from . import state
from ..settings import CONF_OPT
from ..utillib.misc import Point, Cast, ImagePair
from ..viewlib.dialog import Dialog
from ..viewlib.console import MENU_BAR


class GamePack(CSI.French_Deck, object):
    """
    Load and shuffle card / tile packs for a game.
    """
    texts, s = {}, {}
    allstacks = []
    app = preview = canvas = move_hints = top = _view_scale = None

    def __init__(self):
        """
        Initialize the card pack.

        Set mix-in class variables. Call next method in resolution order.

        .. \__init__() -> None

        *Variables*:
            images: {NPSImages}
              Set as None.
            random: {Random(MTRandom|LCRandom)}
              Set as None.
        """
        self.__cards = []
        self.images = self.random = None

        super(GamePack, self).__init__()

    @state.game_busy
    def prepare(self, progress=None):
        """
        Prepare pack for current game.

        Call next method in order. Reset random state. Set required pack detail.

        .. prepare_game(progress:Progress=None) -> None

        *Parameters*:
           progress: {Progess} None
             When given value is not none, update progress.

        *Requires*:
          Methods:
           CARD_DB.request()

          Variables:
            Game.images - When game is preview, call images subsample
            method to reduce images by half.

            Game.random - Reset as None for new initial card shuffle.
        """
        ancestor = super(GamePack, self)
        if hasattr(ancestor, 'prepare'):
            ancestor.prepare(progress)

        self.random = None

        # Load pack from player choice.
        request_type = self['category']
        while not CARD_DB.request_imageset(request_type, progress):
            Dialog.warning(text=_('The current pack {} is not compatible.'
                                  ).format(CARD_DB.current['name']))
            # TODO: fix chooser to be from dialog to avoid MENU_BAR import
            MENU_BAR._open_cardset_chooser(CARD_DB.current['index'])
            request_type = CARD_DB.current['type']

        self.images = CARD_DB.current_imageset
        if self.is_preview:
            self.images = self.images.subsample(self.images, ratio=2)
            self.images.bottom_light(CONF_OPT['pile_bottom'])
        else:
            self.top.event_generate('<<update_cardback_menu>>')

    def _pack_create(self, progress=None):
        """
        Build the card pack for game.

        Using game details add card for each rank/suit in each deck. When
        detail has trumps, add each trump for each deck. When detail has bonus,
        add each bonus for each set of deck.

        .. \_pack_create(coords:seq(int, int), progress:Progress=None) -> None

        *Parameters*:
           progress: {Progess} None
             When given value is not none, update progress.

        *Raises:
          AssertionError:
              When pack size not equal detail number of cards.
        """
        coord = Point(self.s['talon'].x, self.s['talon'].y,)

        pstep = 0
        if progress:
            pstep = (100.0 - progress.percent) / self['ncards']

        cards = []

        def add_card(deck, suit, rank):
            """
            Append card to pack.
            """
            cast = Cast(deck=deck, suit=suit, rank=rank,
                       color=self._get_color(suit), force=self._get_force(suit))
            card_ = CardItem(self.canvas, len(cards), coord, cast=cast,
                              image=ImagePair(back=self.images.back,
                                         face=self.images.face(cast._asdict())))
            if card_ is None:
                return
            cards.append(card_)
            if progress:
                progress.update(step=pstep)

        for deck in range(self['decks']):
            for suit in self['suits']:
                for rank in self['ranks']:
                    add_card(deck=deck, suit=suit, rank=rank)
            trump_suit = len(self['suits'])
            for rank in self['trumps']:
                add_card(deck=deck, suit=trump_suit, rank=rank)

        # Accomodate packs like mahjongg tiles
        if self['bonus']:
            start = rank + 1
            for deck in range(self['si']['decks']):
                for rank in self['bonus']:
                    add_card(deck=deck, suit=trump_suit, rank=rank + start)

        if progress:
            progress.update(percent=100)

        assert len(cards) == self['ncards']
        self.__cards = tuple(cards)

    def _pack_shuffle(self, cards, parts=1):
        """
        Return randomly shuffled cards.

        Shuffle of given parts of card/tile pack (eg. decks within a pack).

        *Note:* Any overide should only shuffle or not shuffle given cards.

        .. \__pack_shuffle(cards:seq(Card ...)[, parts:int=1]) -> seq(Card ...)

        *Parameters*:
          cards : {sequence(Card ...)}
            Current sequence of cards before shuffle.
          parts : {number} 1
            Number of parts to shuffle seperate. Used by games that shuffle
            decks individually.
        """
        pack = []
        size = len(cards)
        ncards = size // parts
        for index in range(0, size, ncards):
            part = cards[index:index + ncards]
            self.random.shuffle(part)
            pack.extend(part)
        return pack

    def _pack_place(self, cards):
        """
        Return implicitly placed cards.

        Stub to return cards repositioned for deal on specific stacks.

        *Note:* Any overide should only position or not position given cards.

        .. \_pack_place(cards:seq(Card ...), random:Random) -> seq(Card ...)

        *Parameters:*
          cards : {sequence(Card ...)}
            Current sequence of cards after shuffle.
        """
        return cards

    def do_talon_init(self, random_class):
        """
        Initialize talon with shuffled pack.

        Create new random seed based on given random class. Call pack shuffle
        with copy of original pack then call pack place to position cards for
        proper deal and play. Final call talon addCard method for each card
        to place pack on talon pile face down.

        *Note*: Method should not be overrode. Use pack shuffle and pack place.

        .. do_talon_init(random_class:Random) -> None

        *Parameters*:
          random_class : {Random(MTRandom|LCRandom)}
            Random class to generate card shuffle sequence. Given value maybe
            None to create new Random class.

        *Variables:*
          s['talon']: {Pile}
          Call its addCard method to add card then card's show back method
          with unhide False.

        *Raises*:
          AssertionError:
            When s.['talon'] not subclass of ..pilelib.Talon.

            When s.['talon'],cards count not equal Game['ncards'].
        """
        from ..packlib.shuffle import update_random_state
        from ..pilelib import Talon

        self.random = update_random_state(random_class or self.random)
        cards = self._pack_shuffle(list(self.__cards))
        cards = self._pack_place(cards)
        talon = self.s['talon']

        assert talon is not None and issubclass(talon.__class__, Talon)

        for card in cards:
            talon.addCard(card, update=False)
            card.show_back(unhide=False)

        assert len(talon.cards) == self['ncards']

    def _pack_reset(self):
        """
        Reset canvas for game.

        Remove cards from canvas. Reset all piles. When preview, reset score
        and base rank text items.

        .. \__canvas_reset() -> None

        *Variables:*
          allstacks: {sequence(Pile ...)}
           Call pile method
          preview: {boolean}
            Game is preview dialog.
          texts: {mapping}
            When preview, Reset score and base rank item text value.
        """
        self.move_hint.reset()
        self.s['talon'].removeAllCards()
        for pile in self.allstacks:
            pile.reset()
        if self.is_preview:
            for text_item in (self.texts[k] for k in ['score', 'base_rank']
                                            if self.texts[k]):
                text_item['text'] = ''

    def reset(self, restart=False):
        """
        Reset current game.

        Augment to pack reset method to return cards to talon and reset piles.
         Call next method in order. Called by internal new and restore methods.

        .. reset([restart:bool=False]):

        *Parameters*:
          restart : {bool} False
            Allow some attributes to survive a reset, but not a restart.
        """
        ancestor = super(GamePack, self)
        if hasattr(ancestor, 'reset'):
            ancestor.reset(restart)
        self._pack_reset()

    @state.move_state('restore')
    @state.game_busy
    def restore(self, data, reset=True):
        """
        Restore prior game details.

        Augement to set cards and pile capability with given data. Call next
        method in order.

        .. restore(data:GameData[, reset:bool=True, autoplay:bool:True]) -> None

        *Parameters*:
          data: {GameData}
            Data to restore game pile card and capability details.
          reset: {boolean} True
            Reset current game before restore.

        *Variables:*
          random: {NPSRandom}
            Set given game.random attribute value.
          allstacks: {sequence(Pile ...)}
           Update pile cards and capabilities
          s['talon']: {Pile}
            Update its round attribute.

        *Raises*:
          AssertionError:
            When allstacks not same as piles to restore.
        """
        self._pack_reset()
        self.random = data['seed']
        del data['seed']
        assert data['num_piles'] == len(self.allstacks)
        del data['num_piles']
        for pile, (pid, cap, cards) in zip(self.allstacks, data['piles']):
            assert pid == pile.id
            pile.cap.update(**cap)
            for card_id, face_up in cards:
                card = self.__cards[card_id]
                if face_up:
                    card.show_face()
                else:
                    card.show_back()
                pile.addCard(card)
        del data['piles']
        self.s['talon'].round = data['cur_round']
        del data['cur_round']
        ancestor = super(GamePack, self)
        if hasattr(ancestor, 'restore'):
            ancestor.restore(data, reset)

    def _view_resize(self):
        """
        Resize view of canvas items.

        Call images resize method with current view scale. Update pack for new
        image size. Call next method in order.

        .. \_view_resize(self) -> None

        *Variables:*
            images: {Images}
        """
        scale = self._view_scale
        # images
        self.images.resize(*scale)
        # cards
        for card in self.__cards:
            card.update(self.canvas, card.coord, face=self.images.face(card),
                        back=self.images.back)

        ancestor = super(GamePack, self)
        if hasattr(ancestor, '_view_resize'):
            ancestor._view_resize()

    def nextgame_flags(self, id_, random_class=None):
        """
        Return flag for next game difference.

        Compare current pack and seed with next. Calls next method in order.

        .. nextgame_flags(id_:int[, random_class:Random=None]) -> int

        *Parameters* :
          flags : {number}
            Flag to indicate game difference between current and next.

            0 - no change.

            1 - id change.

            2 - cardset change.

            3 - id and cardset change.

            16 - seed change.

            17 - id and seed change.

            18 - cardset and seed change.

            19 - id, cardset and seed change.

          random_class : {Random|None}
            Compare current random with given.

        *Variables:*
          images: {NPSImages}
            Compare between current and next CARD_DB.current detail.
          random - {Random(MTRandom|LCRandom)}
            Compare between current and next seed for type and value.
        """
        flags = 0
        ancestor = super(GamePack, self)
        if hasattr(ancestor, 'nextgame_flags'):
            flags = ancestor.nextgame_flags(id_, random_class=random_class)
        if CARD_DB.current is not self.images.detail:
            flags |= 2
        if (random_class is not None and
             any([random_class.__class__ is not self.random.__class__,
                 random_class._initseed != self.random._initseed])):
            flags |= 16
        return flags

    def _save_stack_caps(self, piles, **kwargs):
        """
        Set piles' capabilities passed as keywords for persistance.

         Used for games that assign pile capabilities after start.

        .. \_save_stack_caps(piles:seq(Pile ...)[*pile capabilty keywords*])
           -> None

        *Parameters*:
            pile: {Pile | sequence}
               Given pile[s] have given capability keywords applied.
        """
        if not isinstance(piles, (list, tuple)):
            piles = [piles]
        for pile in piles:
            pile.cap.update(**kwargs)